using System;
using System.Collections.Generic;
using Fadd.Data.Mappings;
using Fadd.Parser;
using Fadd.Parser.SimpleSQL.Tokens;

namespace Fadd.Data
{
    /// <summary>
    /// Compiles and caches SQL statements to speed up processing.
    /// </summary>
    internal class StatementCompiler
    {
        private readonly bool _cacheStatements;
        private readonly Provider _providers;
        private readonly Dictionary<string, CompiledStatement> _statements = new Dictionary<string, CompiledStatement>();

        /// <summary>
        /// Initializes a new instance of the <see cref="StatementCompiler"/> class.
        /// </summary>
        /// <param name="providers">The providers.</param>
        /// <param name="cacheStatements">if set to <c>true</c> [cache statements].</param>
        public StatementCompiler(Provider providers, bool cacheStatements)
        {
            Check.Require(providers, "providers");

            _providers = providers;
            _cacheStatements = cacheStatements;
        }

        /// <summary>
        /// Compiles an object SQL statement
        /// </summary>
        /// <param name="objectSql"></param>
        /// <returns></returns>
        /// <exception cref="FormatException"><c>FormatException</c>.</exception>
        /// <exception cref="MappingException">If mapping could not be found.</exception>
        private CompiledStatement Compile(string objectSql)
        {
            Parser.SimpleSQL.Parser parser = new Parser.SimpleSQL.Parser();
            ParseTree parseTree = parser.Parse(objectSql);

            string className = null;
            foreach (Token token in parseTree.Root.Children)
            {
                if (!(token is From)) continue;
                foreach (Token child in token.Children)
                {
                    if (!(child is Class)) continue;
                    className = ((Class) child).Name;
                    break;
                }
                break;
            }

            if (className == null)
                throw new FormatException("Failed to find class name in statement: " + objectSql);

            List<string> conditions = new List<string>();
            List<string> properties = new List<string>();
            IMapping mapping = _providers.GetMapping(className);
			if (mapping == null)
				throw new MappingException(null, "Could not find mapping for: " + className);
            foreach (Token token in parseTree.Root.Children)
            {
                if (token is Select)
                {
                    foreach (Token child in token.Children)
                    {
                        if (!(child is Parser.SimpleSQL.Tokens.Property)) continue;
                        Parser.SimpleSQL.Tokens.Property property = (Parser.SimpleSQL.Tokens.Property) child;
                        if (property.Name == "*")
                            continue;
                        properties.Add(property.Name);
                    }
                }
                if (token is Where)
                    ScanProperties(token, conditions);

                ParseToken(token, mapping);
            }
            string sql = parseTree.ToString();
            return new CompiledStatement(mapping, sql, properties.Count == 0 ? null : properties, conditions);
        }

        /// <summary>
        /// Recursive scanning after properties.
        /// </summary>
        /// <param name="token"></param>
        /// <param name="conditions"></param>
        private static void ScanProperties(Token token, ICollection<string> conditions)
        {
            if (token is PropertyValue)
            {
                PropertyValue propertyValue = (PropertyValue) token;
                conditions.Add(propertyValue.Name);
                propertyValue.Name = "@param" + (conditions.Count - 1);
            }

            foreach (Token child in token.Children)
                ScanProperties(child, conditions);
        }

        internal CompiledStatement CompileWrapper(string objectSql)
        {
            return Compile(objectSql);
        }

        /// <summary>
        /// Compile an Object SQL statement and return the corresponding SQL statement.
        /// </summary>
        /// <param name="key">Key used to identify this statement (if caching is turned on)</param>
        /// <param name="objectSql">SQL statement with classes/properties instead of tables/columns.</param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public CompiledStatement GetOrCreate(string key, string objectSql, params object[] parameters)
        {
            if (!_cacheStatements)
                return Compile(objectSql);

            CompiledStatement statement;
            lock (_statements)
                if (_statements.TryGetValue(key, out statement))
                    return statement;

            return Create(key, objectSql);
        }

        /// <summary>
        /// Goes through a token and all it's children to convert properties/classes to columns/tables.
        /// </summary>
        /// <param name="token">Token to process</param>
        /// <param name="mapping">Mapping file used during conversion.</param>
        private static void ParseToken(Token token, IMapping mapping)
        {
			Check.Require(token, "token");
			Check.Require(mapping, "mapping");

            if (token is Class)
                ((Class) token).Name = mapping.TableName;
            else if (token is Parser.SimpleSQL.Tokens.Property)
            {
                Parser.SimpleSQL.Tokens.Property property = (Parser.SimpleSQL.Tokens.Property) token;
                if (property.Name != "*")
                    property.Name = mapping.GetColumnName(property.Name);
            }
            else if (token is OrderByProperty)
            {
                OrderByProperty property = (OrderByProperty) token;
                property.Name = mapping.GetColumnName(property.Name);
            }

            foreach (Token child in token.Children)
                ParseToken(child, mapping);
        }

        /// <summary>
        /// Get a previously compiled statement.
        /// </summary>
        /// <param name="key">Key used when storing the statement.</param>
        /// <returns>Compiled statement if found; otherwise null.</returns>
        /// <exception cref="InvalidOperationException">If statements are not cached.</exception>
        public CompiledStatement Get(string key)
        {
            if (!_cacheStatements)
                throw new InvalidOperationException("Statements are not cached, Get can therefore not be used.");

            CompiledStatement statement;
            lock (_statements)
                return _statements.TryGetValue(key, out statement) ? statement : null;
        }

        /// <summary>
        /// Create a new compiled statement.
        /// </summary>
        /// <param name="key">Key used to store statement for future use.</param>
        /// <param name="objectSql">ObjectSQL that should be compiled into plain sql.</param>
        /// <returns>Compiled statement.</returns>
        public CompiledStatement Create(string key, string objectSql)
        {
            CompiledStatement statement = Compile(objectSql);
            if (statement != null)
            {
                lock (_statements)
                    if (!_statements.ContainsKey(key))
                        _statements.Add(key, statement);
            }
            return statement;
        }
    }
}